package org.tamal.classinfo;

import java.io.DataInputStream;
import java.io.IOException;

/**
 * Attribute data structure is used to hold different information about a class,
 * field and methods. It is also used in Code data structure which is a type of
 * Attribute data structure.
 *
 * @author Tamal Kanti Nath
 */
public final class Attribute {

    /**
     * The ConstantValue attribute.
     */
    public static final String CONSTANT = "ConstantValue";
    /**
     * The Code attribute.
     */
    public static final String CODE = "Code";
    /**
     * The StackMapTable attribute.
     */
    public static final String STACK_MAP = "StackMapTable";
    /**
     * The Exceptions attribute.
     */
    public static final String EXCEPTIONS = "Exceptions";
    /**
     * The InnerClasses attribute.
     */
    public static final String INNER_CLASSES = "InnerClasses";
    /**
     * The EnclosingMethod attribute.
     */
    public static final String ENCLOSING_METHOD = "EnclosingMethod";
    /**
     * The Synthetic attribute.
     */
    public static final String SYNTHETIC = "Synthetic";
    /**
     * The Signature attribute.
     */
    public static final String SIGNATURE = "Signature";
    /**
     * The SourceFile attribute.
     */
    public static final String SOURCE = "SourceFile";
    /**
     * The SourceDebugExtension attribute.
     */
    public static final String DEBUG = "SourceDebugExtension";
    /**
     * The LineNumberTable attribute.
     */
    public static final String LINE_NUMBER = "LineNumberTable";
    /**
     * The LocalVariableTable attribute.
     */
    public static final String LOCAL_VARIABLE = "LocalVariableTable";
    /**
     * The LocalVariableTypeTable attribute.
     */
    public static final String LOCAL_VARIABLE_TYPE = "LocalVariableTypeTable";
    /**
     * The Deprecated attribute.
     */
    public static final String DEPRECATED = "Deprecated";
    /**
     * The RuntimeVisibleAnnotations attribute.
     */
    public static final String VISIBLE_ANNOTATION = "RuntimeVisibleAnnotations";
    /**
     * The RuntimeInvisibleAnnotations attribute.
     */
    public static final String INVISIBLE_ANNOTATION
        = "RuntimeInvisibleAnnotations";
    /**
     * The RuntimeVisibleParameterAnnotations attribute.
     */
    public static final String VISIBLE_PARAMETER_ANNOTATION
        = "RuntimeVisibleParameterAnnotations";
    /**
     * The RuntimeInvisibleParameterAnnotations attribute.
     */
    public static final String INVISIBLE_PARAMETER_ANNOTATION
        = "RuntimeInvisibleParameterAnnotations";
    /**
     * The AnnotationDefault attribute.
     */
    public static final String ANNOTATION_DEFAULT = "AnnotationDefault";
    /**
     * The BootstrapMethods attribute.
     */
    public static final String BOOTSTRAP_METHODS = "BootstrapMethods";

    private final ClassInfo cls;
    private int nameIndex;
    private int length;
    private Integer constant;
    private Code code;
    private StackMapFrame[] stackMap;
    private Integer[] exceptionIndex;
    private InnerClass[] innerClass;
    private Integer classIndex;
    private Integer methodIndex;
    private Integer signatureIndex;
    private Integer sourceFileIndex;
    private String debugExtension;
    private LineNumber[] lineNumberTable;
    private LocalVariable[] localVariableTable;
    private LocalVariableType[] localVariableTypeTable;
    private Annotation[] visibleAnnotations;
    private Annotation[] invisibleAnnotations;
    private Annotation[][] visibleParameterAnnotations;
    private Annotation[][] invisibleParameterAnnotations;
    private ElementValue defaultValue;
    private BootstrapMethod[] bootstrapMethods;

    private Attribute(DataInputStream in, ClassInfo cls) throws IOException {
        this.cls = cls;
        nameIndex = in.readUnsignedShort();
        String name = cls.getPool()[nameIndex].getStringData();
        length = in.readInt();
        if (CONSTANT.equals(name)) {
            constant = in.readUnsignedShort();
        }
        if (CODE.equals(name)) {
            code = new Code(in, cls);
        }
        if (STACK_MAP.equals(name)) {
            stackMap = StackMapFrame.readStackMapTable(in);
        }
        if (EXCEPTIONS.equals(name)) {
            exceptionIndex = new Integer[in.readUnsignedShort()];
            for (int i = 0; i < exceptionIndex.length; ++i) {
                exceptionIndex[i] = in.readUnsignedShort();
            }
        }
        if (INNER_CLASSES.equals(name)) {
            innerClass = InnerClass.readInnerClasses(in, cls);
        }
        if (ENCLOSING_METHOD.equals(name)) {
            classIndex = in.readUnsignedShort();
            methodIndex = in.readUnsignedShort();
        }
        // The SYNTHETIC attribute do not specify any additional information.
        if (SIGNATURE.equals(name)) {
            signatureIndex = in.readUnsignedShort();
        }
        if (SOURCE.equals(name)) {
            sourceFileIndex = in.readUnsignedShort();
        }
        if (DEBUG.equals(name)) {
            debugExtension = in.readUTF();
        }
        if (LINE_NUMBER.equals(name)) {
            lineNumberTable = LineNumber.readLineNumberTable(in);
        }
        if (LOCAL_VARIABLE.equals(name)) {
            localVariableTable = LocalVariable.readLocalVariableTable(in, cls);
        }
        if (LOCAL_VARIABLE_TYPE.equals(name)) {
            localVariableTypeTable
                = LocalVariableType.readLocalVariableTypeTable(in, cls);
        }
        // The DEPRECATED attribute do not specify any additional information.
        if (VISIBLE_ANNOTATION.equals(name)) {
            visibleAnnotations = Annotation.readAnnotations(in, cls);
        }
        if (INVISIBLE_ANNOTATION.equals(name)) {
            invisibleAnnotations = Annotation.readAnnotations(in, cls);
        }
        if (VISIBLE_PARAMETER_ANNOTATION.equals(name)) {
            visibleParameterAnnotations
                = Annotation.readParameterAnnotations(in, cls);
        }
        if (INVISIBLE_PARAMETER_ANNOTATION.equals(name)) {
            invisibleParameterAnnotations
                = Annotation.readParameterAnnotations(in, cls);
        }
        if (ANNOTATION_DEFAULT.equals(name)) {
            defaultValue = new ElementValue(in, cls);
        }
        if (BOOTSTRAP_METHODS.equals(name)) {
            bootstrapMethods = BootstrapMethod.readBootstrapMethods(in, cls);
        }
    }

    /**
     * Returns name.
     * @return the name.
     */
    public String getName() {
        return cls.getPool()[nameIndex].getStringData();
    }

    /**
     * Returns length.
     * @return the length
     */
    public int getLength() {
        return length;
    }

    /**
     * Returns constantValue.
     * @return the constantValue
     */
    public Constant getConstant() {
        return cls.getPool()[constant];
    }

    /**
     * Returns code.
     * @return the code
     */
    public Code getCode() {
        return code;
    }

    /**
     * Returns stackMap.
     * @return the stackMap
     */
    public StackMapFrame[] getStackMap() {
        return stackMap;
    }

    /**
     * Returns exception index.
     * @return the exceptionIndex
     */
    public Integer[] getExceptionIndex() {
        return exceptionIndex;
    }

    /**
     * Returns exception.
     * @return the exception
     */
    public String[] getException() {
        String[] exception = new String[exceptionIndex.length];
        for (int i = 0; i < exception.length; ++i) {
            exception[i] = cls.getPool()[exceptionIndex[i]].toString();
        }
        return exception;
    }

    /**
     * Returns innerClass.
     * @return the innerClass
     */
    public InnerClass[] getInnerClass() {
        return innerClass;
    }

    /**
     * Returns enclosing class index.
     * @return the enclosing class index
     */
    public Integer getClassIndex() {
        return classIndex;
    }

    /**
     * Returns enclosing method index.
     * @return the enclosing method index
     */
    public Integer getMethodIndex() {
        return methodIndex;
    }

    /**
     * Returns enclosing method.
     * @return the enclosing method
     */
    public String getEnclosingMethod() {
        Constant[] pool = cls.getPool();
        String clsName = pool[classIndex].toString();
        String method = methodIndex == 0 ? null : pool[classIndex].toString();
        return methodIndex == 0 ? clsName : Util.insertClass(clsName, method);
    }

    /**
     * Returns signature.
     * @return the signature
     */
    public Integer getSignatureIndex() {
        return signatureIndex;
    }

    /**
     * Returns signature.
     * @return the signature
     */
    public String getSignature() {
        return cls.getPool()[signatureIndex].getStringData();
    }

    /**
     * Returns source file index.
     * @return the source file index
     */
    public Integer getSourceFileIndex() {
        return sourceFileIndex;
    }

    /**
     * Returns sourceFile.
     * @return the sourceFile
     */
    public String getSourceFile() {
        return cls.getPool()[sourceFileIndex].getStringData();
    }

    /**
     * Returns debugExtension.
     * @return the debugExtension
     */
    public String getDebugExtension() {
        return debugExtension;
    }

    /**
     * Returns lineNumberTable.
     * @return the lineNumberTable
     */
    public LineNumber[] getLineNumberTable() {
        return lineNumberTable;
    }

    /**
     * Returns localVariableTable.
     * @return the localVariableTable
     */
    public LocalVariable[] getLocalVariableTable() {
        return localVariableTable;
    }

    /**
     * Returns localVariableTypeTable.
     * @return the localVariableTypeTable
     */
    public LocalVariableType[] getLocalVariableTypeTable() {
        return localVariableTypeTable;
    }

    /**
     * Returns visibleAnnotations.
     * @return the visibleAnnotations
     */
    public Annotation[] getVisibleAnnotations() {
        return visibleAnnotations;
    }

    /**
     * Returns invisibleAnnotations.
     * @return the invisibleAnnotations
     */
    public Annotation[] getInvisibleAnnotations() {
        return invisibleAnnotations;
    }

    /**
     * Returns invisibleAnnotations.
     * @return the visibleParameterAnnotations
     */
    public Annotation[][] getVisibleParameterAnnotations() {
        return visibleParameterAnnotations;
    }

    /**
     * Returns invisibleParameterAnnotations.
     * @return the invisibleParameterAnnotations
     */
    public Annotation[][] getInvisibleParameterAnnotations() {
        return invisibleParameterAnnotations;
    }

    /**
     * Returns defaultValue.
     * @return the defaultValue
     */
    public ElementValue getDefaultValue() {
        return defaultValue;
    }

    /**
     * Returns bootstrapMethods.
     * @return the bootstrapMethods
     */
    public BootstrapMethod[] getBootstrapMethods() {
        return bootstrapMethods;
    }

    @Override
    public String toString() {
        return getName();
    }

    /**
     * Static method to read attributes from a class.
     * @param in The DataInputStream
     * @param cls The ClassInfo reference
     * @return The array of attributes
     * @throws IOException exception
     * @see Code
     * @see Field
     * @see Method
     * @see ClassInfo
     */
    static Attribute[] readAttributes(DataInputStream in, ClassInfo cls)
        throws IOException {
        Attribute[] attribute = new Attribute[in.readUnsignedShort()];
        for (int i = 0; i < attribute.length; ++i) {
            attribute[i] = new Attribute(in, cls);
        }
        return attribute;
    }
}
